<?php
/*
Plugin Name: Negative Turing Test
Plugin URI: http://code.google.com/p/negative-turing-test/
Description: Attempts to prevent comment spam by making the user perform some negative action (e.g. deletion) according to human-readable instructions.
Version: 0.1
Author: Peter Hosey
Author URI: http://boredzo.org/
*/

load_plugin_textdomain('negative-turing-test', 'wp-content/plugins/negative-turing-test');

###
# Option definitions
###

# Options governing display in the comment form.
add_option('ntt_include_div_wrapper', false, __('Whether the Negative Turing Test section of your comment form should be wrapped in a div. Leaving this off helps prevent spammers from adapting to NTT, but also makes it harder to style the NTT part of the comment form.'), 'no');
add_option('ntt_include_ids', false, __('Whether the elements (including the div wrapper, if that\'s turned on) in the Negative Turing Test section of your comment form should each have an id attribute. Leaving this off helps prevent spammers from adapting to NTT, but also makes it harder to style the NTT part of the comment form.'), 'no');
add_option('ntt_main_string', '', __('The default value of the text field, to be edited by the user.'), 'no');
add_option('ntt_instructions', '', __('Instructions for would-be commenters. You may leave this blank if your main string provides sufficient instruction.'), 'no');

# Options governing the formulation of the computed main string.
add_option('ntt_range_start', '', __('The start of the range of the main string that must be replaced or deleted.'), 'no');
add_option('ntt_range_length', '', __('The length of the range of the main string that must be replaced or deleted. If the length of the range is 0, then the user must simply insert text.'), 'no');
add_option('ntt_param_string', '', __('The string with which the user must replace part of the main string (that part being defined by the range). If this string is empty and the range is non-empty, then the required action is simply deletion; if this string is empty and the range is also empty, then the action is a no-op.'), 'no');
add_option('ntt_tabindex', 4, __('The tab order of the response field. Determines where the NTT response field is in the sequence of fields being focused as the user repeatedly hits tab.'), 'no');

# Options governing comparison of the response to the computed main string.
add_option('ntt_casesensitive', false, __('Whether the response should be compared to the computed main string in a case-sensitive manner. Case-sensitive comparison considers letters in different cases to be inequal; case-insensitive comparison (the default) considers them the same. For example, “A” is in the upper case, whereas “a” is in the lower case; they are the same letter when compared case-insensitively, and they are different letters when compared case-sensitively.'), 'no');

# Options governing disposal of comments that fail the test.
add_option('ntt_disposal_action', 'spam', __('The action to take on the comment if the remote side failed the Turing test. The default is "spam", which marks the comment as spam for the education of other spam-fighting tools such as Akismet. Alternatively, it can be "delete", which throws the comment straight into the nearest memory hole.'), 'no');

###
# The real work
###

# Return the string with invisible characters shown as HTML entity references and the BR element.
function ntt_show_invisibles($str) {
	$str = str_replace(' ',  '&middot;', $str);
	$str = str_replace("\t", '&#x21e5;', $str);
	$str = str_replace("\r", '&#x21e4;', $str);
	$str = str_replace("\n", '<br />', $str);
	return $str;
}
# Return the computed main string for the given parameters. Not HTML-escaped.
function ntt_compute($main_string, $range_start, $range_end, $param_string) {
	return substr_replace($main_string, $param_string, $range_start, $range_end);
}

# Returns whether the $response is equal to the computed main string (see ntt_compute).
function ntt_validate($response) {
	$computed_main_string = ntt_compute(get_option('ntt_main_string'), get_option('ntt_range_start'), get_option('ntt_range_length'), get_option('ntt_param_string'));
	if(intval(get_option('ntt_casesensitive')))
		return strcmp($response, $computed_main_string) == 0;
	else
		return strcasecmp($response, $computed_main_string) == 0;
}

###
# Sinks
###

# Add the options page.
function ntt_install_options_page() {
	if (function_exists('add_options_page')) {
		require_once(dirname(__FILE__) . '/options-panel.php');
		add_options_page(__('Negative Turing Test options'), __('Negative Turing Test'), 'manage_options', dirname(__FILE__) . '/options-panel.php', ntt_show_options);
	} else {
		?><p>Warning! The add_options_page function does not exist!</p><?php
	}
}
add_action('admin_menu', 'ntt_install_options_page');

# Inject our Turing-test form elements into the comment form.
function ntt_install_turing_test($postID) {
	?><fieldset id="negative_turing_test">
	<p><label for="ntt_response"><?php _e('Spam-prevention question'); ?>: </label>
	<input type="text" name="ntt_response" size="50" value="<?php echo htmlspecialchars(get_option('ntt_main_string')); ?>" <?php
	if(get_option('ntt_tabindex'))
		echo 'tabindex="' . get_option('ntt_tabindex') . '" ';
	?>/></p>
<?php
	$instructions = get_option('ntt_instructions');
	if($instructions) {
		?>
	<p><?php echo htmlspecialchars($instructions); ?></p><?php
	}
?>
	</fieldset>
<?php
}
add_action('comment_form_identity', 'ntt_install_turing_test');

# Generate the computed main string, and determine whether the submitted main string matches it.
function ntt_test_for_humanity($comment) {
	global $ntt_client_is_human;
	$ntt_client_is_human = is_user_logged_in() || ntt_validate($_POST['ntt_response']);
	return $comment;
}
add_filter('preprocess_comment', 'ntt_test_for_humanity', 1);

# Reject the comment if the commenter is not human (as determined by the commenter's response to the challenge).
function ntt_report_humanity($approved) {
	global $ntt_client_is_human;
	if(!$ntt_client_is_human)
		update_option('ntt_number_of_spams_blocked', get_option('ntt_number_of_spams_blocked') + 1);
	return $ntt_client_is_human ? true : 'spam';
}
add_filter('pre_comment_approved', 'ntt_report_humanity');

# If we've already determined that the remote side is not human, and our disposal action is deletion, delete it.
function ntt_comment_post($comment_ID) {
	global $ntt_client_is_human;
	if($ntt_client_is_human) {
		# Obliterate the cookie when the user successfully posts a comment, since he won't want to see that comment text in the form again.
		setcookie('ntt_saved_comment_text',
			      '',
			      time() - 3600, # Expire one hour before now, so that the browser considers it expired and deletes it.
			      parse_url(get_option('siteurl'), PHP_URL_PATH)
				  );
	} else {
		# Stash the comment text as a cookie, which we'll retrieve for the comment form.
		# A global won't work because these are two separate sessions (the comment form action, and the page with the new comment form), which the global does not survive between.
		setcookie('ntt_saved_comment_text',
			      get_comment($comment_ID)->comment_content,
			      time() + 60, # Expire sixty seconds from now. This cookie doesn't need to last long.
			      parse_url(get_option('siteurl'), PHP_URL_PATH)
				  );

		if(get_option('ntt_disposal_action') == 'delete')
			wp_set_comment_status($comment_ID, 'delete');
	}
}
add_filter('comment_post', 'ntt_comment_post');

# If $ntt_saved_comment_text is non-empty, the user failed the challenge, and has been dumped at a fresh comment form. Since the user failed the challenge, we should present the user with his comment, just in case he typo'd or forgot to respond to the challenge or something.
function ntt_comment_form($post_ID) {
	# When this hook is called, the comment textarea has already been inserted into the page, so we must send a JavaScript that will put the comment into it.
	# JavaScript can access cookies, too, but it's harder, requiring either manual examination of the cookie string or a utility function to be sent to the client. That would be a waste of bandwidth, so I simply retrieve the cookie value here and embed the value directly into the JavaScript script as a literal.

	$saved_comment_text = $_COOKIE['ntt_saved_comment_text'];

	# Do we have a saved comment?
	if (isset($saved_comment_text) && !empty($saved_comment_text)) {
		# Quote it for JavaScript.
		$quoted = addcslashes($saved_comment_text, "'\\\x00..\x19");
		# Construct the script and send it.
?><script type="text/javascript">
document.getElementById('comment').value = '<?php echo $quoted; ?>';
</script>
<?php
	}
}
add_action('comment_form', 'ntt_comment_form');

?>
